<?php

/* +----------------------------------------------------------------------+
 * | SMS_Clickatell                                                       |
 * +----------------------------------------------------------------------+
 * | Copyright (c) 2002-2005 Jacques Marneweck                            |
 * +----------------------------------------------------------------------+
 * | This source file is subject to version 3.0 of the PHP license,       |
 * | that is bundled with this package in the file LICENSE, and is        |
 * | available at through the world-wide-web at                           |
 * | http://www.php.net/license/3_0.txt.                                  |
 * | If you did not receive a copy of the PHP license and are unable to   |
 * | obtain it through the world-wide-web, please send a note to          |
 * | license@php.net so we can mail you a copy immediately.               |
 * +----------------------------------------------------------------------+
 * | Authors: Jacques Marneweck <jacques@php.net>                         |
 * | Authors: Donald A. Lobo <lobo@civicrm.org>
 * +----------------------------------------------------------------------+
 */

require_once 'PEAR.php';

/**
 * PHP Interface into the Clickatell API
 *
 * Made some pretty major changes. rewrote using our standards and also functionalized the code a bit
 *
 * @author	Jacques Marneweck <jacques@php.net>
 * @copyright	2002-2005 Jacques Marneweck
 * @license	http://www.php.net/license/3_0.txt	PHP License
 * @version	$Id: Clickatell.php,v 1.31 2005/12/18 14:24:08 jacques Exp $
 * @access	public
 * @package	SMS
 */

require_once 'CRM/SMS/Protocol.php';

class CRM_SMS_Protocol_Clickatell extends CRM_SMS_Protocol {

	/**
	 * Clickatell API Server Session ID
     *
	 * @var string
	 */
	protected $_sessionID = null;

	/**
	 * Curl handle resource id
     *
	 */
	protected $_ch;

	/**
	 * Temporary file resource id
	 * @var	resource
	 */
	protected $_fp;

	/**
	 * Error codes generated by Clickatell Gateway
	 * @var array
	 */
	protected $errors = array (
		'001' => 'Authentication failed',
		'002' => 'Unknown username or password',
		'003' => 'Session ID expired',
		'004' => 'Account frozen',
		'005' => 'Missing session ID',
		'007' => 'IP lockdown violation',
		'101' => 'Invalid or missing parameters',
		'102' => 'Invalid UDH. (User Data Header)',
		'103' => 'Unknown apismgid (API Message ID)',
		'104' => 'Unknown climsgid (Client Message ID)',
		'105' => 'Invalid Destination Address',
		'106' => 'Invalid Source Address',
		'107' => 'Empty message',
		'108' => 'Invalid or missing api_id',
		'109' => 'Missing message ID',
		'110' => 'Error with email message',
		'111' => 'Invalid Protocol',
		'112' => 'Invalid msg_type',
		'113' => 'Max message parts exceeded',
		'114' => 'Cannot route message',
		'115' => 'Message Expired',
		'116' => 'Invalid Unicode Data',
		'201' => 'Invalid batch ID',
		'202' => 'No batch template',
		'301' => 'No credit left',
		'302' => 'Max allowed credit'
	);

	/**
	 * Message status
	 *
	 * @var array
	 */
	protected $messageStatus = array (
		'001' => 'Message unknown',
		'002' => 'Message queued',
		'003' => 'Delivered',
		'004' => 'Received by recipient',
		'005' => 'Error with message',
		'006' => 'User cancelled message delivery',
		'007' => 'Error delivering message',
		'008' => 'OK',
		'009' => 'Routing error',
		'010' => 'Message expired',
		'011' => 'Message queued for later delivery',
		'012' => 'Out of credit'
	);

	protected $messageType = array (
		'SMS_TEXT',
		'SMS_FLASH',
		'SMS_NOKIA_OLOGO',
		'SMS_NOKIA_GLOGO',
		'SMS_NOKIA_PICTURE',
		'SMS_NOKIA_RINGTONE',
		'SMS_NOKIA_RTTL',
		'SMS_NOKIA_CLEAN',
		'SMS_NOKIA_VCARD',
		'SMS_NOKIA_VCAL'
	);

    /**
     * We only need one instance of this object. So we use the singleton
     * pattern and cache the instance in this variable
     *
     * @var object
     * @static
     */
    static private $_singleton = null;

    /**
     * Constructor
     *
     * Create and auth a Clickatell session.
     *
     * @return void
     */
    function __construct( ) {
        // first create the curl handle
		/**
		 * Reuse the curl handle
		 */
        $this->_ch = curl_init();
        if (!$this->_ch || !is_resource($this->_ch)) {
            return PEAR::raiseError('Cannot initialise a new curl handle.');
        }
		
        curl_setopt($this->_ch, CURLOPT_TIMEOUT, 20);
        curl_setopt($this->_ch, CURLOPT_VERBOSE, 1);
        curl_setopt($this->_ch, CURLOPT_FAILONERROR, 1);
        curl_setopt($this->_ch, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($this->_ch, CURLOPT_COOKIEJAR, "/dev/null");
        curl_setopt($this->_ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($this->_ch, CURLOPT_USERAGENT, 'CiviCRM 2.0 - http://civicrm.org/');
        
        $this->authenticate( );
    }

    /**
     * singleton function used to manage this object
     *
     * @return object
     * @static
     *
     */
    static function &singleton( ) {
        if (self::$_singleton === null ) {
            self::$_singleton = new CRM_SMS_Protocol_Clickatell( );
        }
        return self::$_singleton;
    }

	/**
	 * Authenticate to the Clickatell API Server.
	 *
	 * @return mixed true on sucess or PEAR_Error object
	 * @access public
	 * @since 1.1
	 */
	function authenticate( ) {
        $config = CRM_Core_Config::singleton( );
		$url = $config->smsAPIServer . "/http/auth";

		$postData = "user=" . $config->smsUsername . "&password=" . $config->smsPassword . "&api_id=" . $config->smsAPIID;

		$response = $this->curl($url, $postData);
		if (PEAR::isError($response)) {
			return $response;
		}
		$sess = explode(":", $response['data']);

		$this->_sessionID = trim($sess[1]);

		if ($sess[0] == "OK") {
			return (true);
		} else {
			return PEAR::raiseError($response['data']);
		}
	}

    function formURLPostData( $url, $id = null ) {
        $config = CRM_Core_Config::singleton( );
		$url = $config->smsAPIServer . $url;
		$postData = "session_id=" . $this->_sessionID;
        if ( $id ) {
            if (strlen($id) < 32 || strlen($id) > 32) {
                return PEAR::raiseError('Invalid API Message Id');
            }
            $postData .= "&apimsgid=" . $id;
        }
        return array( $url, $postData );
    }

	/**
	 * Delete message queued by Clickatell which has not been passed
	 * onto the SMSC.
	 *
	 * @access	public
	 * @since	1.14
	 * @see		http://www.clickatell.com/downloads/Clickatell_http_2.2.2.pdf
	 */
	function deleteMessage ( $id ) {
        list( $url, $postData ) = $this->formURLPostData( "/http/delmsg", $id );

		$response = $this->curl($url, $postData);
		if (PEAR::isError($response)) {
			return $response;
		}
		$sess = explode(":", $response['data']);

		$deleted = preg_split("/[\s:]+/", $response['data']);
		if ($deleted[0] == "ID") {
			return (array($deleted[1], $deleted[3]));
		} else {
			return PEAR::raiseError($response['data']);
		}
	}

	/**
	 * Query balance of remaining SMS credits
	 *
	 * @access	public
	 * @since	1.9
	 */
	function getBalance () {
        list( $url, $postData ) = $this->formURLPostData( "/http/getbalance" );

        $response = $this->curl($url, $postData);
		if (PEAR::isError($response)) {
			return $response;
		}
		$send = explode(":", $response['data']);

		if ($send[0] == "Credit") {
			return trim($send[1]);
		} else {
			return PEAR::raiseError($response['data']);
		}
	}

	/**
	 * Determine the cost of the message which was sent
	 *
	 * @param	string	$id
	 * @since	1.20
	 * @access  public
	 */
	function getMessageCharge( $id ) {
        list( $url, $postData ) = $this->formURLPostData( "/http/getmsgcharge", $id );

		$response = $this->curl($url, $postData);
		if (PEAR::isError($response)) {
			return $response;
		}
		$charge = preg_split("/[\s:]+/", $response['data']);

		if ($charge[2] == "charge") {
			return (array($charge[3], $charge[5]));
		}

		/**
		 * Return charge and message status
		 */
		return (array($charge[3], $charge[5]));
	}

	/**
	 * Keep our session to the Clickatell API Server valid.
	 *
	 * @return mixed true on sucess or PEAR_Error object
	 * @access public
	 * @since 1.1
	 */
	function ping () {
        list( $url, $postData ) = $this->formURLPostData( "/http/ping" );

		$response = $this->curl($url, $postData);
		if (PEAR::isError($response)) {
			return $response;
		}
		$sess = explode(":", $response['data']);

		if ($sess[0] == "OK") {
			return (true);
		} else {
			return PEAR::raiseError($response['data']);
		}
	}

	/**
	 * Query message status
	 *
	 * @param string spimsgid generated by Clickatell API
	 * @return string message status or PEAR_Error object 
	 * @access public
	 * @since 1.5
	 */
	function queryMessage ($id) {
        list( $url, $postData ) = $this->formURLPostData( "/http/querymsg", $id );

		$response = $this->curl($url, $postData);
		if (PEAR::isError($response)) {
			return $response;
		}
		$status = explode(" ", $response['data']);

		if ($status[0] == "ID:") {
			return (trim($status[3]));
		} else {
			return PEAR::raiseError($response['data']);
		}
	}

	/**
	 * Send an SMS Message via the Clickatell API Server
	 *
	 * @param array the message with a to/from/text
	 *
	 * @return mixed true on sucess or PEAR_Error object
	 * @access public
	 * @since 1.2
	 */
	function sendMessage ( &$message ) {
        list( $url, $postData ) = $this->formURLPostData( "/http/sendmsg" );

        $postData .= "&from=" . $message['From'];
        $postData .= "&to="   . $message['To'];
        $postData .= "&text=" . $message['Body'];
        $postData .= "&climsgid=" . $message['id'];
        $postData .= "&callback=3&deliv_ack=1";

		if (!in_array($message['Type'], $this->messageType)) {
			return PEAR::raiseError("Invalid message type. Message ID is " . $message['id']);
		}

		if ($message['Type'] != "SMS_TEXT") {
			$postData .= "&msg_type=" . $message['Type'];
		}

		/**
		 * Check if we are using a queue when sending as each account
		 * with Clickatell is assigned three queues namely 1, 2 and 3.
		 */
		if (isset($message['queue']) && is_numeric($message['queue'])) {
			if (in_array($message['queue'], range(1, 3))) {
				$postData .= "&queue=" . $message['queue'];
			}
		}

		$reqFeat = 0;

		/**
		 * Normal text message
		 */
		if ($message['Type'] == 'SMS_TEXT') {
			$reqFeat += 1;
		}

		/**
		 * We set the sender id is alpha numeric or numeric
		 * then we change the sender from data.
		 */
		if (is_numeric($message['From'])) {
			$reqFeat += 32;
		} elseif (is_string($message['From'])) {
			$reqFeat += 16;
		}

		/**
		 * Flash Messaging
		 */
		if ($message['msg_type'] == 'SMS_FLASH') {
			$reqFeat += 512;
		}

		/**
		 * Delivery Acknowledgments
		 */
		$reqFeat += 8192;

		if (!empty($reqFeat)) {
			$postData .= "&req_feat=" . $reqFeat;
		}

		/**
		 * Must we escalate message delivery if message is stuck in
		 * the queue at Clickatell?
		 */
		if (isset($message['escalate']) && !empty($message['escalate'])) {
			if (is_numeric($message['escalate'])) {
				if (in_array($message['escalate'], range(1, 2))) {
					$postData .= "&escalate=" . $message['escalate'];
				}
			}
		}

		$response = $this->curl($url, $postData);
		if (PEAR::isError($response)) {
			return $response;
		}
		$send = explode(":", $response['data']);
        
		if ($send[0] == "ID") {
			return true;
		} else {
			return PEAR::raiseError($response['data']);
		}
	}

	/**
	 * Spend a clickatell voucher which can be used for topping up of
	 * sub user accounts.
	 *
	 * @param	string	voucher number
	 * @access	public
	 * @since	1.22
	 * @see		http://www.clickatell.com/downloads/Clickatell_http_2.2.4.pdf
	 */
	function pay ($voucher) {
        list( $url, $postData ) = $this->formURLPostData( "/http/token_pay" );

		$postData  .= "&token=" . trim($voucher);

		if (!is_numeric($voucher) || strlen($voucher) < 16 || strlen($voucher) > 16) {
			return (PEAR::raiseError('Invalid voucher number'));
		}

		$response = $this->curl($url, $postData);
		if (PEAR::isError($response)) {
			return $response;
		}
		$sess = explode(":", $response['data']);

		$paid = preg_split("/[\s:]+/", $response['data']);
 		if ($paid[0] == "OK") {
			return true; 
		} else {
			return PEAR::raiseError($response['data']);
		}
	}

	/**
	 * Perform curl stuff
	 *
	 * @param   string  URL to call
	 * @param   string  HTTP Post Data
	 * @return  mixed   HTTP response body or PEAR Error Object
	 * @access	private
	 */
	function curl ($url, $postData) {

		$this->_fp = tmpfile();

		curl_setopt($this->_ch, CURLOPT_URL, $url);
		curl_setopt($this->_ch, CURLOPT_POST, 1);
		curl_setopt($this->_ch, CURLOPT_POSTFIELDS, $postData);
		curl_setopt($this->_ch, CURLOPT_FILE, $this->_fp);

		$status = curl_exec($this->_ch);
		$response['http_code'] = curl_getinfo($this->_ch, CURLINFO_HTTP_CODE);

		if (empty($response['http_code'])) {
			return PEAR::raiseError ('No HTTP Status Code was returned.');
		} elseif ($response['http_code'] === 0) {
			return PEAR::raiseError ('Cannot connect to the Clickatell API Server.');
		}

		if ($status) {
			$response['error'] = curl_error($this->_ch);
			$response['errno'] = curl_errno($this->_ch);
		}

		rewind($this->_fp);

		$pairs = "";
		while ($str = fgets($this->_fp, 4096)) {
			$pairs .= $str;
		}
		fclose($this->_fp);

		$response['data'] = $pairs;
		unset($pairs);
		asort($response);

		return ($response);
	}
}
